﻿using System;
using System.Collections.Generic;
using System.Linq;
using ICommonLibrary.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace ICommonLibrary.Collection {

    [JsonConverter(
        typeof(VTableJsonConvert)
    )]
    public sealed class VTable {

        private static readonly Column[] EmptyColumns = new Column[0];
        private static readonly JsonValue[] EmptyValues = new JsonValue[0];

        private string _name;
        private Column[] _columns;
        private LookupType _lookupType;
        private Dictionary<string, Column> _columnIndex;
        private LookupStrategy _lookupStrategy;

        public VTable() {
            _name = string.Empty;
            _columns = EmptyColumns;
        }

        public VTable(string name, Column[] columns, LookupType lookupType) : this() {
            Name = name;
            Reconstruct(columns, lookupType);
        }

        public void Reconstruct(Column[] columns, LookupType lookupType) {
            _columns = columns;
            _lookupType = lookupType;
            Index();
        }

        public String Name {
            get {
                return _name;
            }
            set {
                Asserts.NotNull(value, "Table name can not be null");
                _name = value;
            }
        }

        public LookupType GetLookupType() {
            return _lookupType;
        }

        public Column GetColumn(int index) {
            return index >= 0 && index < _columns.Length ? _columns[index] : null;
        }

        public Column GetColumn(string columnName) {
            if (columnName == null) return null;

            Column column;
            if (_columnIndex.TryGetValue(columnName, out column)) {
                return column;
            }
            return null;
        }

        public Column[] GetColumns() {
            return _columns;
        }

        public int GetColumnCount() {
            return _columns.Length;
        }

        /// <summary>
        /// Return the first column. Null is returned if the number of columns is 0.
        /// </summary>
        /// <returns></returns>
        public Column GetIndexColumn() {
            return _columns.Length == 0 ? null : _columns[0];
        }

        /// <summary>
        /// Return empty array if the number of columns is 0.
        /// </summary>
        /// <returns></returns>
        public JsonValue[] GetIndexColumnValues() {
            Column indexColumn = GetIndexColumn();
            return indexColumn != null ? indexColumn.Values : EmptyValues;
        }

        public int GetRowCount() {
            if (_columns.Length == 0) {
                return 0;
            }
            return _columns[0].Values.Length;
        }

        public JsonValue VLookup(JsonValue searchCriterion, string targetColumnName) {
            return _lookupStrategy.Lookup(searchCriterion, targetColumnName);
        }

        public VTable Clone() {
            return JsonConvert.DeserializeObject<VTable>(JsonConvert.SerializeObject(this));
        }

        public override int GetHashCode() {
            return 9934 + _name.GetHashCode();
        }

        public override bool Equals(object o) {
            if (o == null) return false;
		    if (o == this) return true;
    		
		    if (o.GetType() == typeof(VTable)) {
			    var t = (VTable) o;
			    return 	_name.Equals(t._name) &&
					    _lookupType == t._lookupType &&
					    ArrayUtil.Equals(_columns, t._columns);
		    }
		    return false;
        }

        private void Index() {
            _columnIndex = _columns.ToDictionary(c => c.Name);
            _lookupStrategy =  _lookupType == LookupType.RangeLookup ? 
                               new RangeLookupStrategy(this) :
                               (LookupStrategy) new EqualMatchingStrategy(this);
        }

        public sealed class Column {

            private string _name;
            private JsonValueType _valueType;
            private JsonValue[] _values;

            public Column() {
                _name = string.Empty;
                _values = EmptyValues;
            }
           
            public string Name {
                get { return _name; } 
                set {
                    Asserts.NotNull(value, "Column name must not be null");
                    _name = value;
                }
            }

            public JsonValueType ValueType {
                get { return _valueType; }
                set {
                    Asserts.NotNull(value, "Column value type must not be null");
                    _valueType = value;
                }
            }

            public JsonValue[] Values {
                get { return _values; }
                set {
                    Asserts.NotNull(_values, "Column values must not be null");
                    _values = value;
                }
            }

            public override int GetHashCode() {
                return 9483 + _name.GetHashCode();
            }

            public override bool Equals(object o) {
                if (o == null) return false;
		        if (o == this) return true;
        		
		        if (o.GetType() == typeof(Column)) {
                    var c = (Column)o;
                    return  _name.Equals(c._name) &&
                            _valueType == c._valueType &&
                            ArrayUtil.Equals(_values, c._values);
		        }
		        return false;
            }

        }

        public enum LookupType {
            EqualLookup = 0,
            RangeLookup = 1
        }

        private abstract class LookupStrategy {

            protected VTable Table { get; private set; }

            protected LookupStrategy(VTable table) {
                Table = table;
            }

            protected Column GetColumn(string columnName) {
                Column column = Table.GetColumn(columnName);
                if (column == null) {
                    throw new Exception("Column#" + columnName + " doesn't exist.");
                }
                return column;
            }

            public abstract JsonValue Lookup(JsonValue searchCriterion, string targetColumnName);

        }

        private class EqualMatchingStrategy : LookupStrategy {

            private Dictionary<string, int> _index; // GetIndexKey(variable) => row index

            public EqualMatchingStrategy(VTable table) : base(table) {
                Index();
            }

            public override JsonValue Lookup(JsonValue searchCriterion, string targetColumnName) {
                int rowIndex;
                if (_index.TryGetValue(GetIndexKey(searchCriterion), out rowIndex)) {
                    return GetColumn(targetColumnName).Values[rowIndex];
                }
                return JsonNull.Instance;
            }

            private void Index() {
                JsonValue[] values = Table.GetIndexColumnValues();

                _index = new Dictionary<string, int>();
                int length = values.Length;
                for (int i = 0; i < length; i++) {
                    JsonValue value = values[i];
                    _index[GetIndexKey(value)] = i;
                }
            }

            private static string GetIndexKey(JsonValue value) {
                return Util.GetString(value.ToJsonString());
            }

        }

        private class RangeLookupStrategy : LookupStrategy {

            private readonly JsonValue[] _index;

            public RangeLookupStrategy(VTable table) : base(table) {
                _index = Table.GetIndexColumnValues();
            }

            public override JsonValue Lookup(JsonValue searchCriterion, string targetColumnName) {
                int index = Array.BinarySearch(_index, searchCriterion, JsonValueComparer.Instance);
                if (index == -1) { // the criterion is smaller than the smallest value of the column
                    return JsonNull.Instance;
                }
                if (index < -1) { // no match and fail in range 
                    index = -(index + 1) - 1;
                }
                return GetColumn(targetColumnName).Values[index];
            }
        }

        private class JsonValueComparer : IComparer<JsonValue> {

            public static readonly JsonValueComparer Instance = new JsonValueComparer();

            public int Compare(JsonValue x, JsonValue y) {
                if (x == null && y == null) return 0;
                if (x == null) return -1;
                if (y == null) return 1;

                JsonNumber numX = x.IsNumber();
                JsonNumber numY = y.IsNumber();
                if (numX == null || numY == null) return 0;

                return numX.Value.CompareTo(numY.Value);
            }
        }

        private class VTableJsonConvert : JsonConverter {

            public override bool CanConvert(Type objectType) {
                return objectType == typeof(VTable);
            }

            public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
                JObject jsonTable = JObject.Load(reader);

                var table = new VTable();
                var lookupType = (LookupType)(int)jsonTable["LookupType"];
                var columns = new List<Column>();

                var jsonColumns = jsonTable["Columns"] as JArray;
                if (jsonColumns != null) {
                    columns.AddRange(jsonColumns.Select(jsonColumn => new Column {
                        Name = Util.GetString((string) jsonColumn["Name"]), 
                        ValueType = (JsonValueType) (int) jsonColumn["ValueType"],
                        Values = EmptyValues
                    }));
                }

                var jsonValues  = jsonTable["Values"] as JArray;
                if (jsonValues != null) {
                    var length = jsonValues.Count;

                    var rownum = (length == 0) ? 0 : length / columns.Count;

                    for (int i = 0; i < length; i += rownum) {
                        var column = columns[i / rownum];
                        var rowValues = new JsonValue[rownum];
                        for (int row = 0; row < rownum; row++) {
                            rowValues[row] = ReadJsonValues(column.ValueType, jsonValues[i + row]);
                        }
                        column.Values = rowValues;
                    }
                }

                table.Name = Util.GetString((String)jsonTable["Name"]);
                table.Reconstruct(columns.ToArray(), lookupType);
                return table;
            }

            private static JsonValue ReadJsonValues(JsonValueType valueType, JToken v) {
                if (valueType == JsonValueType.Boolean) {
                    var b = (bool?)v;
                    return b != null ? new JsonBoolean(b.Value) : (JsonValue)JsonNull.Instance;
                }
                if (valueType == JsonValueType.Number) {
                    var d = (double?)v;
                    return d != null ? new JsonNumber(d.Value) : (JsonValue)JsonNull.Instance;
                }
                if (valueType == JsonValueType.String) {
                    var s = (string)v;
                    return s != null ? new JsonString(s) : (JsonValue)JsonNull.Instance;
                }
                throw new Exception("Can not create value of JsonValueType " + valueType);
            }

            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
                var table = (VTable)value;
                var values = new List<JsonValue>(1000);

                writer.WriteStartObject();
                writer.WriteProperty("Name", table._name);
                writer.WriteProperty("LookupType", (int) table._lookupType);
                
                // 1. write columns
                writer.WritePropertyName("Columns");
                writer.WriteStartArray();
                foreach (var column in table.GetColumns()) {
                    writer.WriteStartObject();
                    writer.WriteProperty("Name", column.Name);
                    writer.WriteProperty("ValueType", (int) column.ValueType);
                    writer.WriteEndObject();

                    values.AddRange(column.Values);
                }
                writer.WriteEndArray();

                // 2. write values
                writer.WritePropertyName("Values");
                writer.WriteStartArray();
                foreach (var v in values) {
                    if (v.IsBoolean() != null) {
                        writer.WriteValue(v.IsBoolean().Value);
                    }
                    else if (v.IsNumber() != null) {
                        writer.WriteValue(v.IsNumber().Value);
                    }
                    else if (v.IsString() != null) {
                        writer.WriteValue(v.IsString().Value);
                    }
                    else if (v.IsNull() != null) {
                        writer.WriteNull();
                    }
                    else {
                        throw new Exception("Can not serialize " + v);
                    }
                }
                writer.WriteEndArray();

                writer.WriteEndObject();
            }

        }
        
    }
}